所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。
由于服务端已经渲染好了 HTML,所以在客户端不需要重新创建全部的 DOM 元素。客户端激活就是激活服务端返回的 HTML,使之成为响应式。
有两种方式标记 HTML 是服务端放返回:
<div id="app" data-server-rendered="true" />
app.$mount("#app", true)
通过上面方式可获取当前 vnode 是否为服务端渲染,在对服务端和客户端的 vnode 进行 patch 的时候,会进一步判断 oldVnode 是否为真实的 DOM 元素节点,是的话则:
- 调用 hydrate 对两端数据进行混合。
- 调用 invokeCreateHooks,激活客户端数据,如:绑定标签的绑定事件等。
- 调用 invokeInsertHooks,调用组件插入钩子。
function createPatchFunction (backend) {
// ...
return function patch(oldVnode, vnode, hydrating, removeOnly) {
// ...
var isRealElement = isDef(oldVnode.nodeType);
if (isRealElement) {
// 判断是否为服务端渲染
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR);
hydrating = true;
}
if (isTrue(hydrating)) {
// 服务端渲染客户端激活逻辑
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true);
return oldVnode
} else {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
);
}
}
}
}
}
# hydrate 函数
function hydrate(elm, vnode, insertedVnodeQueue, inVPre) {
var i;
var tag = vnode.tag;
var data = vnode.data;
var children = vnode.children;
inVPre = inVPre || (data && data.pre);
vnode.elm = elm;
// 判断是否为注释或者异步组件
if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) {
vnode.isAsyncPlaceholder = true;
return true
}
// 检验 node 节点是否匹配
{
if (!assertNodeMatch(elm, vnode, inVPre)) {
return false
}
}
if (isDef(data)) {
// 如果是子组件,则初始化子组件
if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode, true /* hydrating */); }
if (isDef(i = vnode.componentInstance)) {
// child component. it should have hydrated its own tree.
initComponent(vnode, insertedVnodeQueue);
return true
}
}
// 标签节点
if (isDef(tag)) {
if (isDef(children)) {
// empty element, allow client to pick up and populate children
if (!elm.hasChildNodes()) {
// 实际节点没有子节点,客户端的节点有子节点,则创建子节点并添加到旧节点中
createChildren(vnode, children, insertedVnodeQueue);
} else {
// v-html and domProps: innerHTML
if (isDef(i = data) && isDef(i = i.domProps) && isDef(i = i.innerHTML)) {
// v-html 和 domProps,则判断 innerHTML 是否相同
if (i !== elm.innerHTML) {
/* istanbul ignore if */
if (typeof console !== 'undefined' &&
!hydrationBailed
) {
hydrationBailed = true;
console.warn('Parent: ', elm);
console.warn('server innerHTML: ', i);
console.warn('client innerHTML: ', elm.innerHTML);
}
return false
}
} else {
// 迭代并比较两端子列表
var childrenMatch = true;
var childNode = elm.firstChild; // 获取旧节点列表的第一个节点
for (var i$1 = 0; i$1 < children.length; i$1++) {
// 遍历新 vnode 列表
// 如果 childNode 不存在,说明实际节点列表的长度和新 vnode 列表不相等,则混合失败
// 如果有子节点,则递归调用 hydrate 进行混合
// 混合失败直接跳出循环
if (!childNode || !hydrate(childNode, children[i$1], insertedVnodeQueue, inVPre)) {
childrenMatch = false;
break
}
childNode = childNode.nextSibling; // 获取旧下一个兄弟节点
}
// childNode 不为空,说明实际子节点列表比新 vnode 列表多,混合失败
if (!childrenMatch || childNode) {
/* istanbul ignore if */
if (typeof console !== 'undefined' &&
!hydrationBailed
) {
hydrationBailed = true;
console.warn('Parent: ', elm);
console.warn('Mismatching childNodes vs. VNodes: ', elm.childNodes, children);
}
return false
}
}
}
}
// 获取当前 vnode 的绑定信息,如:事件等
if (isDef(data)) {
var fullInvoke = false;
for (var key in data) {
if (!isRenderedModule(key)) { // 判断是否属性是否在客户端已初始化
fullInvoke = true;
invokeCreateHooks(vnode, insertedVnodeQueue); // 绑定事件等
break
}
}
if (!fullInvoke && data['class']) {
// 确保收集深层绑定的 deps 以进行更新
traverse(data['class']);
}
}
} else if (elm.data !== vnode.text) {
// 文本节点且实际文本与新 vnode 的文本内容不同,则直接用更新文本内容
elm.data = vnode.text;
}
return true
}
函数执行逻辑为:
- 首先判断是否为注释或者异步组件,是的话直接返回 true,说明混合成功。
- 接着调用 assertNodeMatch 函数校验实际 DOM 节点与新 vnode 的 node 节点是否匹配,失败的话返回 false,说明混合失败。
- 接着判断是否是子组件,如果是子组件,则调用 initComponent 初始化子组件。
- 如果是标签节点,客户端的节点(新 vnode)有子节点,但实际节点没有子节点,则创建子节点并添加到旧节点中。
- 如果是标签节点,客户端的节点(新 vnode)和实际节点都有子节点:
- 如果是 v-html 和 domProps(render 函数中有该属性) 的话,则判断 innerHTML 是否相同,不相同则返回 false;
- 迭代并比较两端子列表。遍历逻辑为:遍历新 vnode 列表,获取实际节点列表的每一个节点 childNode。如果childNode 不存在,说明实际节点列表的长度和新 vnode 列表不相等,则混合失败;如果有子节点,则递归调用 hydrate 进行混合。循环遍历结束后,如果 childNode 不为空,说明实际子节点列表比新 vnode 列表多,混合失败。
- 在处理完上面两个逻辑之后,标签节点还会通过 vnode.data 获取当前 vnode 的绑定信息,然后通过 isRenderedModule 判断节点属性是否在服务端已经处理过,如果未处理过,如:事件等,则调用 invokeCreateHooks 进行处理。
- 如果是文本节点且实际的节点内容与新 vnode 文本内容不相同,则直接用更新文本内容。
# assertNodeMatch 函数
该函数用于校验实际 DOM 节点与新 vnode 的 node 节点是否匹配。
function assertNodeMatch (node, vnode, inVPre) {
if (isDef(vnode.tag)) {
return vnode.tag.indexOf('vue-component') === 0 || (
!isUnknownElement$$1(vnode, inVPre) &&
vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase())
)
} else {
return node.nodeType === (vnode.isComment ? 8 : 3)
}
}
# isRenderedModule Map
用于判断标签属性是否在客户端已经处理过。
var isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key');
function makeMap (
str,
expectsLowerCase
) {
var map = Object.create(null);
var list = str.split(',');
for (var i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return expectsLowerCase
? function (val) { return map[val.toLowerCase()]; }
: function (val) { return map[val]; }
}
# invokeCreateHooks 函数
用于处理未在服务端处理过的标签属性,如:事件绑定等。
function invokeCreateHooks(vnode, insertedVnodeQueue) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, vnode);
}
i = vnode.data.hook; // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) { i.create(emptyNode, vnode); }
if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
}
}
其中,cbs 在初始化 patch 函数时,会收集所有的事件 hook 到 cbs 中,如下格式:
function createPatchFunction (backend) {
var i, j;
var cbs = {};
var modules = backend.modules;
var nodeOps = backend.nodeOps;
// 收集所有的事件 hook 到 cbs 中
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]]);
}
}
}
}
// cbs 数据内容
cbs = {
activate: [ƒ]
create: (8) [
updateAttrs(oldVnode, vnode),
updateClass(oldVnode, vnode),
updateDOMListeners(oldVnode, vnode),
updateDOMProps(oldVnode, vnode),
updateStyle(oldVnode, vnode),
_enter(_, vnode),
create(_, vnode),
updateDirectives(oldVnode, vnode)
],
destroy: (2) [ƒ, ƒ]
remove: [ƒ]
update: (7) [ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ]
}
举个例子,如下面的 div 绑定了点击事件 tap,还有 attr title 属性。
<template>
<div @click="tap" :title="txt">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'hello'
}
},
computed: {
txt() {
return 'aaa'
}
},
methods: {
tap() {
console.log('>> tap :')
alert('hello')
}
}
}
</script>
编译 template 后的 vnode.data 数据如下:
data = {
attrs: {
title: "aaa"
},
on: {
click: ƒ (),
length: 0
name: "bound tap"
}
}
通过 isRenderedModule 判读只有 on 未在服务端处理过,则会调用 invokeCreateHooks 函数,进而出发 cb.create.updateDOMListeners 进行 DOM 事件绑定注册。
# invokeInsertHook 函数
在执行完 hydrate 函数逻辑之后,会调用:invokeInsertHook(vnode, insertedVnodeQueue, true)
。用于延迟执行插入组件根节点的钩子,直到元素节点被挂在插入后再执行。
function invokeInsertHook(vnode, queue, initial) {
if (isTrue(initial) && isDef(vnode.parent)) {
// 异步执行。由于在服务端已经初始化过了,所以走的是延迟执行的逻辑
vnode.parent.data.pendingInsert = queue;
} else {
// 未初始化过,直接执行
for (var i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i]);
}
}
}
// 在组件初始化时,会判断 pendingInsert 是否存在
// 是的话则将异步队列中的数据添加到 insertedVnodeQueue 后面
// 然后调用 invokeCreateHooks 一并执行。
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
vnode.data.pendingInsert = null;
}
vnode.elm = vnode.componentInstance.$el;
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
setScope(vnode);
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode);
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode);
}
}